 /*******************************************************************************
  * Copyright (c) 2000, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.core.internal.runtime.auth;

 import java.io.*;
 import java.net.URL ;
 import java.util.*;
 import org.eclipse.core.runtime.*;
 import org.eclipse.osgi.util.NLS;

 /**
  * A database that remembers information, such as user-names and
  * passwords. The information is stored in memory and can be saved
  * to disk in an encrypted format. While the API is phrased in terms of
  * URLs, realms and authentication schemes, not all of these must have
  * significant values. For example, if "realm" is not relevant to a
  * particular application, it can be left blank (though not
  * <code>null</code>).
  */
 public class AuthorizationDatabase {
     public static final String PI_RUNTIME_AUTH = "org.eclipse.core.runtime.auth.compatibility"; //$NON-NLS-1$

     /**
      * Status code constant (value 4) indicating the platform could not read
      * some of its metadata.
      */
     public static final int FAILED_READ_METADATA = 4;

     /**
      * Status code constant (value 5) indicating the platform could not write
      * some of its metadata.
      */
     public static final int FAILED_WRITE_METADATA = 5;

     /**
      * Version number for the format of the key-ring file.
      */
     private static final int KEYRING_FILE_VERSION = 1;

     /**
      * A nested hashtable that stores authorization information. The
      * table maps server URLs to realms to authentication schemes to
      * authorization information.
      */
     private Hashtable authorizationInfo = new Hashtable(5);

     /**
      * A hashtable mapping resource URLs to realms.
      */
     private Hashtable protectionSpace = new Hashtable(5);

     private File file = null;
     private String password = null;
     private boolean needsSaving = true;

     /**
      * Creates a new authorization database whose data cannot be saved to
      * disk.
      */
     public AuthorizationDatabase() {
         super();
     }

     /**
      * Creates a new authorization database, or opens an existing one, whose
      * data is, or can be, saved to a file with the given filename. A
      * password must be given to create a new database and an existing
      * database is opened by supplying the password that was given to create
      * it.
      *
      * @param filename the location of the database on disk. For example,
      * "c:/temp/database"
      * @param password the password to access the database. For example,
      * "secret"
      * @exception CoreException if there are problems creating the database.
      * Reasons include:
      * <ul>
      * <li>The database could not be opened because the wrong password was given.
      * <li>The database could not be opened because the specified file is corrupt.
      * </ul>
      */
     public AuthorizationDatabase(String filename, String password) throws CoreException {
         Assert.isNotNull(filename);
         Assert.isNotNull(password);
         this.password = password;
         file = new File(filename);
         load();
     }

     /**
      * Adds the given authorization information to the database. The
      * information is relevant for the specified protection space and the
      * given authorization scheme. The protection space is defined by the
      * combination of the given server URL and realm. The authorization
      * scheme determines what the authorization information contains and how
      * it should be used. The authorization information is a <code>Map</code>
      * of <code>String</code> to <code>String</code> and typically
      * contain information such as usernames and passwords.
      *
      * @param serverUrl the URL identifying the server for this authorization
      * information. For example, "http://www.hostname.com/".
      * @param realm the subsection of the given server to which this
      * authorization information applies. For example,
      * "realm1@hostname.com" or "" for no realm.
      * @param authScheme the scheme for which this authorization information
      * applies. For example, "Basic" or "" for no authorization scheme
      * @param info a <code>Map</code> containing authorization information
      * such as usernames and passwords
      */
     public void addAuthorizationInfo(URL serverUrl, String realm, String authScheme, Map info) {
         Assert.isNotNull(serverUrl);
         Assert.isNotNull(realm);
         Assert.isNotNull(authScheme);
         Assert.isNotNull(info);

         String url = serverUrl.toString();
         Hashtable realmToAuthScheme = (Hashtable) authorizationInfo.get(url);
         if (realmToAuthScheme == null) {
             realmToAuthScheme = new Hashtable(5);
             authorizationInfo.put(url, realmToAuthScheme);
         }

         Hashtable authSchemeToInfo = (Hashtable) realmToAuthScheme.get(realm);
         if (authSchemeToInfo == null) {
             authSchemeToInfo = new Hashtable(5);
             realmToAuthScheme.put(realm, authSchemeToInfo);
         }

         authSchemeToInfo.put(authScheme.toLowerCase(), info);
         needsSaving = true;
     }

     /**
      * Adds the specified resource to the protection space specified by the
      * given realm. All resources at or deeper than the depth of the last
      * symbolic element in the path of the given resource URL are assumed to
      * be in the same protection space.
      *
      * @param resourceUrl the URL identifying the resources to be added to
      * the specified protection space. For example,
      * "http://www.hostname.com/folder/".
      * @param realm the name of the protection space. For example,
      * "realm1@hostname.com"
      */
     public void addProtectionSpace(URL resourceUrl, String realm) {
         Assert.isNotNull(resourceUrl);
         Assert.isNotNull(realm);

         if (!resourceUrl.getFile().endsWith("/")) { //$NON-NLS-1$
 resourceUrl = URLTool.getParent(resourceUrl);
         }

         String oldRealm = getProtectionSpace(resourceUrl);
         if (oldRealm != null && oldRealm.equals(realm)) {
             return;
         }

         String url1 = resourceUrl.toString();
         Enumeration urls = protectionSpace.keys();
         while (urls.hasMoreElements()) {
             String url2 = (String ) urls.nextElement();
             if (url1.startsWith(url2) || url2.startsWith(url1)) {
                 protectionSpace.remove(url2);
                 break;
             }
         }

         protectionSpace.put(url1, realm);
         needsSaving = true;
     }

     /**
      * Removes the authorization information for the specified protection
      * space and given authorization scheme. The protection space is defined
      * by the given server URL and realm.
      *
      * @param serverUrl the URL identifying the server to remove the
      * authorization information for. For example,
      * "http://www.hostname.com/".
      * @param realm the subsection of the given server to remove the
      * authorization information for. For example,
      * "realm1@hostname.com" or "" for no realm.
      * @param authScheme the scheme for which the authorization information
      * to remove applies. For example, "Basic" or "" for no
      * authorization scheme.
      */
     public void flushAuthorizationInfo(URL serverUrl, String realm, String authScheme) {
         Hashtable realmToAuthScheme = (Hashtable) authorizationInfo.get(serverUrl.toString());

         if (realmToAuthScheme == null) {
             return;
         }

         Hashtable authSchemeToInfo = (Hashtable) realmToAuthScheme.get(realm);

         if (authSchemeToInfo == null) {
             return;
         }

         authSchemeToInfo.remove(authScheme.toLowerCase());

         needsSaving = true;
     }

     /**
      * Returns the authorization information for the specified protection
      * space and given authorization scheme. The protection space is defined
      * by the given server URL and realm. Returns <code>null</code> if no
      * such information exists.
      *
      * @param serverUrl the URL identifying the server for the authorization
      * information. For example, "http://www.hostname.com/".
      * @param realm the subsection of the given server to which the
      * authorization information applies. For example,
      * "realm1@hostname.com" or "" for no realm.
      * @param authScheme the scheme for which the authorization information
      * applies. For example, "Basic" or "" for no authorization scheme
      * @return the authorization information for the specified protection
      * space and given authorization scheme, or <code>null</code> if no
      * such information exists
      */
     public Map getAuthorizationInfo(URL serverUrl, String realm, String authScheme) {
         Hashtable realmToAuthScheme = (Hashtable) authorizationInfo.get(serverUrl.toString());
         if (realmToAuthScheme == null) {
             return null;
         }

         Hashtable authSchemeToInfo = (Hashtable) realmToAuthScheme.get(realm);
         if (authSchemeToInfo == null) {
             return null;
         }

         return (Map) authSchemeToInfo.get(authScheme.toLowerCase());
     }

     /**
      * Returns the protection space (realm) for the specified resource, or
      * <code>null</code> if the realm is unknown.
      *
      * @param resourceUrl the URL of the resource whose protection space is
      * returned. For example, "http://www.hostname.com/folder/".
      * @return the protection space (realm) for the specified resource, or
      * <code>null</code> if the realm is unknown
      */
     public String getProtectionSpace(URL resourceUrl) {
         while (resourceUrl != null) {
             String realm = (String ) protectionSpace.get(resourceUrl.toString());
             if (realm != null) {
                 return realm;
             }
             resourceUrl = URLTool.getParent(resourceUrl);
         }

         return null;
     }

     private void load() throws CoreException {
         if (file == null)
             return;
         if (!file.exists()) {
             save();
             return;
         }
         try {
             InputStream input = new FileInputStream(file);
             try {
                 load(input);
             } finally {
                 input.close();
             }
         } catch (IOException e) {
             throw new CoreException(new Status(IStatus.ERROR, PI_RUNTIME_AUTH, FAILED_READ_METADATA, NLS.bind(Messages.meta_unableToReadAuthorization, file), e));
         } catch (ClassNotFoundException e) {
             throw new CoreException(new Status(IStatus.ERROR, PI_RUNTIME_AUTH, FAILED_READ_METADATA, NLS.bind(Messages.meta_unableToReadAuthorization, file), e));
         }
     }

     private void load(InputStream is) throws IOException, ClassNotFoundException , CoreException {
         //try to read the file version number. Pre 2.0 versions had no number
 int version = is.read();
         if (version == KEYRING_FILE_VERSION) {
             //read the authorization data
 CipherInputStream cis = new CipherInputStream(is, password);
             ObjectInputStream ois = new ObjectInputStream(cis);
             try {
                 authorizationInfo = (Hashtable) ois.readObject();
                 protectionSpace = (Hashtable) ois.readObject();
             } finally {
                 ois.close();
             }
         } else {
             //the format has changed, just log a warning
 Activator.log(new Status(IStatus.WARNING, PI_RUNTIME_AUTH, FAILED_READ_METADATA, Messages.meta_authFormatChanged, null));
             //close the stream and save a new file in the correct format
 try {
                 is.close();
             } catch (IOException e) {
                 //ignore failure to close
 }
             needsSaving = true;
             save();
         }
     }

     /**
      * Saves the authorization database to disk.
      */
     public void save() throws CoreException {
         if (!needsSaving || file == null)
             return;
         try {
             file.delete();
             if ((!file.getParentFile().exists() && !file.getParentFile().mkdirs()) || !canWrite(file.getParentFile()))
                 throw new CoreException(new Status(IStatus.ERROR, PI_RUNTIME_AUTH, FAILED_WRITE_METADATA, NLS.bind(Messages.meta_unableToWriteAuthorization, file), null));
             file.createNewFile();
             FileOutputStream out = new FileOutputStream(file);
             try {
                 save(out);
             } finally {
                 out.close();
             }
         } catch (IOException e) {
             throw new CoreException(new Status(IStatus.ERROR, PI_RUNTIME_AUTH, FAILED_WRITE_METADATA, NLS.bind(Messages.meta_unableToWriteAuthorization, file), e));
         }
         needsSaving = false;
     }

     private static boolean canWrite(File installDir) {
         if (!installDir.canWrite())
             return false;

         if (!installDir.isDirectory())
             return false;

         File fileTest = null;
         try {
             fileTest = File.createTempFile("writtableArea", null, installDir); //$NON-NLS-1$
 } catch (IOException e) {
             // If an exception occurred while trying to create the file, it means that it is not writable
 return false;
         } finally {
             if (fileTest != null)
                 fileTest.delete();
         }
         return true;
     }

     private void save(FileOutputStream os) throws IOException {
         //write the version number
 os.write(KEYRING_FILE_VERSION);

         CipherOutputStream cos = new CipherOutputStream(os, password);
         ObjectOutputStream oos = new ObjectOutputStream(cos);
         //write the data
 try {
             oos.writeObject(authorizationInfo);
             oos.writeObject(protectionSpace);
             os.flush();
             os.getFD().sync();
         } finally {
             oos.close();
         }
     }

     /**
      * Sets the password to use for accessing this database. If the database
      * is subsequently saved, this new password is used.
      */
     public boolean setPassword(String oldValue, String newValue) {
         if (!oldValue.equals(password))
             return false;
         password = newValue;
         needsSaving = true;
         return true;
     }
 }

